Skip to content

fix: Unity tool requests no longer overlap during long-running work#1164

Merged
hatayama merged 12 commits into
v3-betafrom
feature/hatayama/serialize-unity-tool-execution
May 18, 2026
Merged

fix: Unity tool requests no longer overlap during long-running work#1164
hatayama merged 12 commits into
v3-betafrom
feature/hatayama/serialize-unity-tool-execution

Conversation

@hatayama
Copy link
Copy Markdown
Owner

Summary

  • Prevent overlapping Unity tool requests from running at the same time.
  • Improve CLI behavior for long-running accepted requests so the initial dispatch timeout does not incorrectly cover the full Unity operation.

User Impact

  • Commands that arrive while Unity is already executing a tool now receive a clear retryable busy response instead of being queued into surprising concurrent work.
  • Long-running Unity operations can continue after the server accepts them without being cut off by the short dispatch timeout.
  • Requests canceled by the CLI or client disconnect release main-thread waits cleanly instead of surfacing as internal JSON-RPC errors.

Changes

  • Added a single-flight Unity tool execution gate with editor busy checks before main-thread dispatch.
  • Split Go client handling between request acceptance and final responses, including bounded final-response waiting and retryable busy envelopes.
  • Preserved the dynamic-code handoff path while keeping ordinary Unity tools serialized.
  • Renamed compilation readiness classes to describe state publishing rather than lock-file behavior.
  • Regenerated checked-in native CLI binaries after the Go client changes.

Verification

  • go run ./cmd/uloop compile --project-path /Users/a12115/ghq/hatayama/unity-cli-loop --force-recompile
  • go run ./cmd/uloop run-tests --project-path /Users/a12115/ghq/hatayama/unity-cli-loop --test-mode EditMode --filter-type regex --filter-value CompilationReadinessStatePublisherTests
  • scripts/check-go-cli.sh && git diff --check
  • codex-review v3-beta

hatayama added 10 commits May 18, 2026 23:37
Reject overlapping Unity tool requests with a machine-readable busy response, propagate request cancellation into tool execution, and teach the Go client to distinguish accepted requests from final responses.
Adopt the UniCli stability pattern more closely by clearing the client deadline after the server accepts a request, while preserving parent cancellation for aborted CLI calls.

Track active Unity client tasks during shutdown, cancel accepted requests when the client disconnects, and add editor-state guards for tools that should not run during compile or update transitions.
Keep a bounded final response wait after dispatch acceptance, avoid consuming application bytes while monitoring accepted clients, and surface editor busy guards as retryable server-busy responses.
Regenerate the native CLI launchers after dispatch-ack client changes so repository Go CLI validation and packaged development binaries stay in sync.
Add the required behavior comments to the new guard tests and remove trailing whitespace from generated meta files so repository diff checks stay clean.
Avoid queuing accepted follow-up requests behind Unity's main-thread dispatcher before the single-flight execution gate can reject them. This keeps blocking tools from letting the next command run after the first command releases the gate.
Allow execute-dynamic-code requests to reach their existing scheduler while keeping other Unity tools single-flight. This preserves foreground preemption of background dynamic-code warmup without allowing unrelated tool execution to interleave.
Restore main-thread dispatch for CLI-only bridge commands after moving the generic JSON-RPC hop behind the tool execution gate. Internal commands still touch Unity APIs but do not pass through the regular tool execution service.
Client disconnect cancellation could leave a queued main-thread switch waiting for the next Unity editor tick and turn normal cancellation into a JSON-RPC internal error. Resume queued switches from cancellation as well as the editor queue, and let OperationCanceledException bubble to the bridge shutdown path.
The component writes readiness state during Unity compilation rather than enforcing a lock file. Rename the facade, publisher, and tests so future cleanup can distinguish external readiness state from tool execution serialization.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 20e74d99-0aca-493f-ba7f-09f99b6f13b4

📥 Commits

Reviewing files that changed from the base of the PR and between 65c8145 and 951098e.

⛔ Files ignored due to path filters (4)
  • Assets/Tests/Editor/BridgeClientConnectionTests.cs.meta is excluded by none and included by none
  • Packages/src/Cli~/dist/darwin-amd64/uloop is excluded by !**/dist/** and included by none
  • Packages/src/Cli~/dist/darwin-arm64/uloop is excluded by !**/dist/** and included by none
  • Packages/src/Cli~/dist/windows-amd64/uloop.exe is excluded by !**/dist/**, !**/*.exe and included by none
📒 Files selected for processing (4)
  • Assets/Tests/Editor/BridgeClientConnectionTests.cs
  • Packages/src/Cli~/internal/cli/error_envelope.go
  • Packages/src/Cli~/internal/cli/error_envelope_test.go
  • Packages/src/Editor/Infrastructure/BridgeTransportListener.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • Packages/src/Cli~/internal/cli/error_envelope.go

📝 Walkthrough

Walkthrough

This PR threads CancellationToken throughout tool execution and introduces two-phase request/response flows in the JSON-RPC protocol. Tool execution is now serialized with editor-state precondition checks, and error handling distinguishes server-busy and disconnect-after-accept scenarios across both native CLI and editor bridges.

Changes

Cancellation-Aware Tool Execution Protocol

Layer / File(s) Summary
Tool execution contract: cancellation token signature
Packages/src/Editor/ToolContracts/IUnityCliLoopTool.cs, Packages/src/Editor/ToolContracts/UnityCliLoopTool.cs, Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs
IUnityCliLoopTool.ExecuteAsync now requires CancellationToken parameter; UnityCliLoopTool<TSchema, TResponse> dispatches cancellation to type-safe implementations; new TOOL_NAME_CONTROL_PLAY_MODE constant added.
RPC request/response: two-phase flow and early response
Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs, Packages/src/Editor/Infrastructure/Api/JsonRpcRequest.cs
ProcessRequest accepts CancellationToken and delegates to ProcessRequestWithEarlyResponseAsync; reads acceptsDispatchAck from incoming requests and emits "accepted" dispatch responses conditionally; new ServerBusyErrorData and JsonRpcErrorTypes.ServerBusy for tool-busy error responses.
CLI client two-phase protocol: accept and response timeouts
Packages/src/Cli~/internal/unityipc/client.go, Packages/src/Cli~/internal/unityipc/outcome.go
Client now supports acceptTimeout and responseTimeout; dials with accept-phase context, reads initial "accepted" response, cancels accept timeout, then reads final response with response-phase deadline; request includes acceptsDispatchAck: true; UnitySendOutcome adds RequestAccepted field.
CLI error envelope: server busy and disconnect-after-accept
Packages/src/Cli~/internal/cli/error_envelope.go, Packages/src/Cli~/internal/cli/error_envelope_test.go
classifyError routes "server_busy" RPC payloads to new unityServerBusyError constructor; adds errorCodeUnityDisconnectedAfterAccept for disconnect-after-accept scenarios; writeToolFailure distinguishes RequestAccepted vs RequestDispatched states.
Tool execution service: single-flight serialization and editor state guard
Packages/src/Editor/Application/UnityCliLoopToolExecutionService.cs, Packages/src/Editor/Application/UnityCliLoopToolBusyException.cs, Packages/src/Editor/Application/UnityCliLoopEditorStateGuard.cs
Service uses internal lock and TryEnterExecution to serialize tool runs; throws UnityCliLoopToolBusyException for concurrent non-dynamic-code tools; EditorStateGuard.Validate checks isCompiling and isUpdating before execution, restricting dynamic-code and play-mode-control tools.
Main thread switching with cancellation support
Packages/src/Editor/Application/MainThreadSwitcher.cs
SwitchToMainThread now accepts optional CancellationToken; MainThreadSwitchContinuation registers cancellation callbacks and uses interlocked state to guarantee single invocation with proper cleanup.
API handler and service registration: cancellation threading and compilation readiness
Packages/src/Editor/Infrastructure/Api/UnityApiHandler.cs, Packages/src/Editor/Application/CompilationReadinessService.cs, Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs, Packages/src/Editor/Infrastructure/InfrastructureEditorStartup.cs, Packages/src/Editor/Infrastructure/Server/CompilationReadinessStatePublisher.cs
ExecuteCommandAsync accepts and threads CancellationToken through internal command and tool paths; CompilationLockService renamed to CompilationReadinessService with updated interface and implementation; service registration and startup wiring updated throughout.
Bridge server: early response and disconnect monitoring
Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs, Packages/src/Editor/Infrastructure/BridgeTransportListener.cs
Server tracks client handler tasks in ConcurrentDictionary; calls ProcessRequestWithEarlyResponseAsync via new ProcessRequestFrameAsync; writes responses through WriteJsonResponseAsync; monitors IsConnected in background to cancel request on disconnect; connection transports pass connectivity predicates.
Error translation: busy exception handling
Packages/src/Editor/Application/ImprovedErrorHandler.cs
DefaultErrorTranslator recognizes UnityCliLoopToolBusyException and returns user-facing "tool is busy" response; DefaultErrorFormatter assigns Medium severity to busy exceptions.
Test suite updates: explicit cancellation tokens throughout
Assets/Editor/FindGameObjects/FindGameObjectsTestMenu.cs, Assets/Tests/Editor/*.cs, Assets/Tests/PlayMode/*.cs
All test files systematically updated to pass explicit CancellationToken.None; new tests added for editor state guard, JSON-RPC concurrency/dispatcher routing, and CLI error envelope classification; test helpers include BuildToolRequest, timeout/cancellation utilities, CapturingMainThreadDispatcher, and tool stubs.
CLI test validation: two-phase flow and error envelope
Packages/src/Cli~/internal/unityipc/client_test.go, Packages/src/Cli~/internal/cli/error_envelope_test.go
Go tests verify SendWithProgressOutcome processes "accepted" before final response, handles timeouts on delayed responses, and respects parent cancellation; error envelope tests validate server-busy and disconnect-after-accept classifications.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • hatayama/unity-cli-loop#1136: Related work on CLI readiness/recovery-state and server-busy/stale recovery handling.
  • hatayama/unity-cli-loop#1079: Also modifies UnityApiHandler.ExecuteCommandAsync wiring; this PR threads CancellationToken through the handler while that PR hardcoded CancellationToken.None in some paths.
  • hatayama/unity-cli-loop#1156: Overlaps on FindGameObjectsTestMenu.cs edits (this PR adds explicit CancellationToken usage; the other PR removes the file).
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly summarizes the main objective: preventing overlapping Unity tool requests during long-running work, which aligns with the substantial changes to add single-flight execution gates and improve timeout handling.
Description check ✅ Passed The PR description is directly related to the changeset, providing clear user impact, detailed change summary, and verification steps that correspond to the implementation changes throughout the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/hatayama/serialize-unity-tool-execution

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs (1)

640-654: 💤 Low value

Consider passing cancellation token to Task.Delay for faster cleanup.

When StopClientDisconnectMonitorAsync cancels the token, the monitor will complete on the next iteration. However, without a cancellation token on Task.Delay, it must wait up to 100ms for the delay to complete naturally before checking IsCancellationRequested.

♻️ Proposed improvement
 private static async Task MonitorClientDisconnectAsync(
     BridgeClientConnection client,
     CancellationTokenSource requestCancellationTokenSource)
 {
-    while (!requestCancellationTokenSource.IsCancellationRequested)
+    try
     {
-        if (!client.IsConnected)
+        while (!requestCancellationTokenSource.IsCancellationRequested)
         {
-            requestCancellationTokenSource.Cancel();
-            return;
-        }
+            if (!client.IsConnected)
+            {
+                requestCancellationTokenSource.Cancel();
+                return;
+            }
 
-        await Task.Delay(ClientDisconnectMonitorPollMilliseconds);
+            await Task.Delay(ClientDisconnectMonitorPollMilliseconds, requestCancellationTokenSource.Token);
+        }
     }
+    catch (OperationCanceledException)
+    {
+        // Expected when request completes normally
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs` around lines
640 - 654, MonitorClientDisconnectAsync currently uses
Task.Delay(ClientDisconnectMonitorPollMilliseconds) without a cancellation
token, so cancelling requestCancellationTokenSource doesn't wake the delay
immediately; update MonitorClientDisconnectAsync to pass
requestCancellationTokenSource.Token into Task.Delay
(Task.Delay(ClientDisconnectMonitorPollMilliseconds,
requestCancellationTokenSource.Token)) and handle
OperationCanceledException/TaskCanceledException by returning (or wrap the await
in a try/catch and return on cancellation). This touches
MonitorClientDisconnectAsync and uses the existing
ClientDisconnectMonitorPollMilliseconds and requestCancellationTokenSource
symbols to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs`:
- Around line 640-654: MonitorClientDisconnectAsync currently uses
Task.Delay(ClientDisconnectMonitorPollMilliseconds) without a cancellation
token, so cancelling requestCancellationTokenSource doesn't wake the delay
immediately; update MonitorClientDisconnectAsync to pass
requestCancellationTokenSource.Token into Task.Delay
(Task.Delay(ClientDisconnectMonitorPollMilliseconds,
requestCancellationTokenSource.Token)) and handle
OperationCanceledException/TaskCanceledException by returning (or wrap the await
in a try/catch and return on cancellation). This touches
MonitorClientDisconnectAsync and uses the existing
ClientDisconnectMonitorPollMilliseconds and requestCancellationTokenSource
symbols to locate the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c2580ff2-3a1f-4ba0-a6f4-d3cf6496cf92

📥 Commits

Reviewing files that changed from the base of the PR and between 1c66210 and 65c8145.

⛔ Files ignored due to path filters (9)
  • Assets/Tests/Editor/CompilationReadinessStatePublisherTests.cs.meta is excluded by none and included by none
  • Assets/Tests/Editor/UnityCliLoopEditorStateGuardTests.cs.meta is excluded by none and included by none
  • Packages/src/Cli~/dist/darwin-amd64/uloop is excluded by !**/dist/** and included by none
  • Packages/src/Cli~/dist/darwin-arm64/uloop is excluded by !**/dist/** and included by none
  • Packages/src/Cli~/dist/windows-amd64/uloop.exe is excluded by !**/dist/**, !**/*.exe and included by none
  • Packages/src/Editor/Application/CompilationReadinessService.cs.meta is excluded by none and included by none
  • Packages/src/Editor/Application/UnityCliLoopEditorStateGuard.cs.meta is excluded by none and included by none
  • Packages/src/Editor/Application/UnityCliLoopToolBusyException.cs.meta is excluded by none and included by none
  • Packages/src/Editor/Infrastructure/Server/CompilationReadinessStatePublisher.cs.meta is excluded by none and included by none
📒 Files selected for processing (33)
  • Assets/Editor/FindGameObjects/FindGameObjectsTestMenu.cs
  • Assets/Tests/Editor/CompilationReadinessStatePublisherTests.cs
  • Assets/Tests/Editor/FindGameObjectsToolTests.cs
  • Assets/Tests/Editor/GetHierarchyToolTests.cs
  • Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs
  • Assets/Tests/Editor/ToolSkillSynchronizerTests.cs
  • Assets/Tests/Editor/UnityCliLoopEditorStateGuardTests.cs
  • Assets/Tests/Editor/UnityCliLoopToolRegistryTests.cs
  • Assets/Tests/PlayMode/SimulateKeyboardTests.cs
  • Assets/Tests/PlayMode/SimulateMouseInputTests.cs
  • Assets/Tests/PlayMode/SimulateMouseUiTests.cs
  • Packages/src/Cli~/internal/cli/error_envelope.go
  • Packages/src/Cli~/internal/cli/error_envelope_test.go
  • Packages/src/Cli~/internal/unityipc/client.go
  • Packages/src/Cli~/internal/unityipc/client_test.go
  • Packages/src/Cli~/internal/unityipc/outcome.go
  • Packages/src/Editor/Application/CompilationReadinessService.cs
  • Packages/src/Editor/Application/ImprovedErrorHandler.cs
  • Packages/src/Editor/Application/MainThreadSwitcher.cs
  • Packages/src/Editor/Application/UnityCliLoopEditorStateGuard.cs
  • Packages/src/Editor/Application/UnityCliLoopToolBusyException.cs
  • Packages/src/Editor/Application/UnityCliLoopToolExecutionService.cs
  • Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs
  • Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs
  • Packages/src/Editor/Infrastructure/Api/JsonRpcRequest.cs
  • Packages/src/Editor/Infrastructure/Api/UnityApiHandler.cs
  • Packages/src/Editor/Infrastructure/BridgeTransportListener.cs
  • Packages/src/Editor/Infrastructure/InfrastructureEditorStartup.cs
  • Packages/src/Editor/Infrastructure/Server/CompilationReadinessStatePublisher.cs
  • Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs
  • Packages/src/Editor/ToolContracts/IUnityCliLoopTool.cs
  • Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs
  • Packages/src/Editor/ToolContracts/UnityCliLoopTool.cs

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 42 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs">

<violation number="1" location="Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs:646">
P2: Handle disposed-client checks in the disconnect monitor; `client.IsConnected` can throw during teardown and currently faults the monitor task.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

{
while (!requestCancellationTokenSource.IsCancellationRequested)
{
if (!client.IsConnected)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Handle disposed-client checks in the disconnect monitor; client.IsConnected can throw during teardown and currently faults the monitor task.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs, line 646:

<comment>Handle disposed-client checks in the disconnect monitor; `client.IsConnected` can throw during teardown and currently faults the monitor task.</comment>

<file context>
@@ -553,6 +584,88 @@ private string CreateContentLengthFrame(string jsonContent)
+        {
+            while (!requestCancellationTokenSource.IsCancellationRequested)
+            {
+                if (!client.IsConnected)
+                {
+                    requestCancellationTokenSource.Cancel();
</file context>
Suggested change
if (!client.IsConnected)
bool isConnected;
try
{
isConnected = client.IsConnected;
}
catch (ObjectDisposedException)
{
isConnected = false;
}
if (!isConnected)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 951098e8.

Changed BridgeClientConnection.IsConnected so a disposed underlying socket or pipe is treated as disconnected instead of faulting the client disconnect monitor task.

Also added an EditMode test covering the disposed connection-state provider case.

hatayama added 2 commits May 19, 2026 02:06
Accepted Unity requests can outlive the final response deadline while Unity may still be running the command. Surface that timeout as a retryable response-waiting error instead of a generic internal CLI failure, and refresh the checked-in native CLI binaries.
Client disconnect monitoring can race with transport teardown after an accepted request. Return false from BridgeClientConnection.IsConnected when the underlying socket or pipe has already been disposed, and cover that contract with an EditMode test.
@hatayama hatayama merged commit c5a583b into v3-beta May 18, 2026
10 checks passed
@hatayama hatayama deleted the feature/hatayama/serialize-unity-tool-execution branch May 18, 2026 23:18
@github-actions github-actions Bot mentioned this pull request May 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant